Stock Market

by Neel Shah

Setup

Import Statements

The import statements and setup functions required to run this program are located below.

In [1]:
import warnings
warnings.filterwarnings("ignore")

import ipywidgets as widgets
from ipywidgets import interact, interact_manual
from IPython.display import Javascript, display
import ipython_blocking

from datetime import datetime, timedelta
import numpy as np

import pandas as pd
import pandas_datareader.data as web

import talib
from talib.abstract import *

import matplotlib.pyplot as plt
%matplotlib inline

import plotly.offline as py
py.init_notebook_mode(connected = False)
import plotly.express as px 
import plotly.graph_objects as go
from plotly.subplots import make_subplots

User Input

Below, the user should input a stock ticker symbol (the code used to identify a stock or cryptocurrency). Some good examples are:

  • GS
  • LYFT
  • UBER
  • TSLA
  • BRK-B
  • BTC-USD

The code will resume once the Start button is clicked.

In [2]:
ticker = widgets.Text(description = 'Stock Ticker')
button = widgets.Button(description = 'Start')
box = widgets.VBox(children = [ticker, button])
box
In [5]:
%blockrun button

Data

Read Data

The code below uses pandas-datareader to get information about the selected stock from Yahoo Finance.

In [6]:
stock = web.DataReader(ticker.value, 'yahoo').reset_index()
stock = round(stock, 2)
stock['Volume'] = stock['Volume'].round(0).astype(int)
stock
Out[6]:
Date High Low Open Close Volume Adj Close
0 2015-02-10 184.80 182.35 183.35 184.56 2556300 171.20
1 2015-02-11 188.19 183.75 184.07 187.65 4079600 174.07
2 2015-02-12 190.00 187.40 188.25 189.78 3086700 176.04
3 2015-02-13 191.33 188.34 189.87 189.00 2720400 175.32
4 2015-02-17 190.63 188.31 188.78 190.02 2123300 176.27
... ... ... ... ... ... ... ...
1253 2020-02-03 242.39 238.05 238.36 239.01 2733000 239.01
1254 2020-02-04 243.74 241.55 242.88 241.94 3052500 241.94
1255 2020-02-05 245.13 243.00 244.99 244.30 3126600 244.30
1256 2020-02-06 245.77 241.18 245.35 241.82 2225600 241.82
1257 2020-02-07 240.52 236.55 239.75 238.00 3006500 238.00

1258 rows × 7 columns

Simple Moving Averages (SMA)

The code below calculates the simple moving average for the stock price over the past 200 days (SMA200) and past 50 days (SMA50).

In [7]:
stock['SMA200'] = talib.SMA(stock['Close'], timeperiod = 200)
stock['SMA50'] = talib.SMA(stock['Close'], timeperiod = 50)

Moving Average Convergence Divergence (MACD)

  • The code below calculates the exponential moving average for the stock price over the past 26 days (EMA26) and 12 days (EMA12). 26 days is the slow period and 12 days is the fast period.
  • The moving average convergence divergence (MACD) is found by calculating the difference between EMA12 and EMA26.
  • The signal (MACDSignal) is calculated by finding the 9 day exponential moving average of the MACD.
  • The histogram value (MACDHist) is calculated by finding the difference between MACD and MACDSignal
In [8]:
stock['EMA26'] = talib.EMA(stock['Close'], timeperiod = 26)
stock['EMA12'] = talib.EMA(stock['Close'], timeperiod = 12)
stock['EMA9'] = talib.EMA(stock['Close'], timeperiod = 9)
stock['MACD'], stock['MACDSignal'], stock['MACDHist'] = talib.MACD(stock['Close'], fastperiod = 12, slowperiod = 26, signalperiod = 9)
stock['MACDHist_SMA'] = talib.SMA(stock['MACDHist'], timeperiod = 10)

Relative Strength Index (RSI)

The code below calculates the relative strength index using a 14 day period. Relative sterength is calculated by doing: $RS=\frac{avg. gain}{avg. loss}$. RSI then uses the following formula: $RSI=100-\frac{100}{1+RS}$.

In [9]:
stock['RSI'] = talib.RSI(stock['Close'], timeperiod = 14)
stock['RSI_SMA'] = talib.SMA(stock['RSI'], timeperiod = 10)

On-Balance Volume (OBV)

The on-balance volume indicator measures cumulative buying/selling pressure by adding the volume on up days and subtracting volume on down days. It can be considered a running total of the stock's trading volume.

In [10]:
stock['OBV'] = talib.OBV(stock['Close'], stock['Volume']).round(0).astype(int)
stock['OBV_SMA'] = talib.SMA(stock['OBV'], timeperiod = 10)

Display Data/Calculations

The code below displays the retrieved stock data and all of the calculations performed.

In [11]:
stock = round(stock, 2)
stock
Out[11]:
Date High Low Open Close Volume Adj Close SMA200 SMA50 EMA26 EMA12 EMA9 MACD MACDSignal MACDHist MACDHist_SMA RSI RSI_SMA OBV OBV_SMA
0 2015-02-10 184.80 182.35 183.35 184.56 2556300 171.20 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 2556300 NaN
1 2015-02-11 188.19 183.75 184.07 187.65 4079600 174.07 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 6635900 NaN
2 2015-02-12 190.00 187.40 188.25 189.78 3086700 176.04 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 9722600 NaN
3 2015-02-13 191.33 188.34 189.87 189.00 2720400 175.32 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 7002200 NaN
4 2015-02-17 190.63 188.31 188.78 190.02 2123300 176.27 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 9125500 NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1253 2020-02-03 242.39 238.05 238.36 239.01 2733000 239.01 212.15 231.87 238.99 241.35 241.22 2.36 3.76 -1.40 -0.55 50.37 57.85 22441200 23373980.0
1254 2020-02-04 243.74 241.55 242.88 241.94 3052500 241.94 212.33 232.35 239.21 241.44 241.37 2.23 3.46 -1.22 -0.75 54.27 56.47 25493700 23255610.0
1255 2020-02-05 245.13 243.00 244.99 244.30 3126600 244.30 212.53 232.87 239.59 241.88 241.95 2.30 3.22 -0.93 -0.89 57.19 55.23 28620300 23246810.0
1256 2020-02-06 245.77 241.18 245.35 241.82 2225600 241.82 212.72 233.30 239.75 241.87 241.93 2.12 3.00 -0.88 -1.00 53.34 53.98 26394700 23296130.0
1257 2020-02-07 240.52 236.55 239.75 238.00 3006500 238.00 212.91 233.61 239.62 241.28 241.14 1.66 2.73 -1.08 -1.08 47.98 53.03 23388200 23332540.0

1258 rows × 20 columns

Graphs

Candlestick

The interactive candlestick graph below shows the daily variation in stock price. The boxes represent the spread between opening and closing prices, while the lines represent the spread between the low and high prices for each day.

In [12]:
fig = go.Figure(data = [go.Candlestick(x = stock['Date'],
               open = stock['Open'],
               high = stock['High'],
               low = stock['Low'],
               close = stock['Close'])])
fig.update_layout(title_text = 'Candlestick', 
                  xaxis_title = 'Date',
                  yaxis_title = 'Price',
                  xaxis_rangeslider_visible = True)
fig.update_yaxes(nticks = 10)
fig.show()

Simple Moving Average (SMA) Crossovers

The interactive line graph below shows the closing price, the 200-day simple moving average, and the 50-day simple moving average for the stock. Moving averages show what is hapenning to the price of a stock (on average) over time:

  • A buy signal occurs when the 50-day moving average crosses above the 200-day moving average.
  • A sell signal occurs when the 50-day moving average drops below the 200-day moving average.
In [13]:
fig = go.Figure()
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'))
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['SMA200'], name = '200-Day Simple Moving Average'))
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['SMA50'], name = '50-Day Simple Moving Average'))
fig.update_layout(title_text = 'Simple Moving Average (SMA) Crossovers', 
                  xaxis_title = 'Date',
                  yaxis_title = 'Price',
                  xaxis_rangeslider_visible = True)
fig.update_yaxes(nticks = 10)
fig.show()

Moving Average Convergence Divergence (MACD)

The interactive graph below shows the moving average convergence divergence (MACD), signal line (MACDSignal), and histogram (MACDHist).

A buy signal occurs when:

  • MACD is positive (EMA12 > EMA26).
  • MACD crosses above the signal line.

A sell signal occurs when:

  • MACD is negative (EMA26 > EMA12).
  • MACD crosses below the signal line.

The histogram serves as a warning, not a signal:

  • If the histogram value is positive but falling, it signifies that the uptrend may be weakening.
  • If the histogram value is negative but rising, it signifies that the downtrend may be weakening.
In [14]:
fig = make_subplots(rows = 2, cols = 1, shared_xaxes = True)

fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'), row = 1, col = 1)

fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([0]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'black', dash = 'dot')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['MACD'], name = 'Moving Average Convergence Divergence'), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['MACDSignal'], name = 'MACD Signal Line'), row = 2, col = 1)
fig.add_trace(go.Bar(x = stock['Date'], y = stock['MACDHist'], name = 'MACD Histogram', marker_color = 'black'), row = 2, col = 1)

fig.update_layout(title_text = 'Moving Average Convergence Divergence (MACD)', height = 800)

fig.update_xaxes(title_text = 'Date', row = 2, col = 1)
fig.update_yaxes(title_text = 'Price', row = 1, col = 1)
fig.update_yaxes(title_text = 'MACD', row = 2, col = 1)
fig.update_yaxes(nticks = 10, row = 1, col = 1)
fig.update_yaxes(nticks = 10, row = 2, col = 1)
fig.update_yaxes(fixedrange = True, row = 1, col = 1)
fig.update_yaxes(fixedrange = True, row = 2, col = 1)

fig.show()

Relative Strength Index (RSI)

The interactive line graph below shows the relative strength index (RSI) for the stock. RSI values can indicate whether or not a stock is due for a correction:

  • If RSI > 70 (the red region of the graph), the stock is overbought and due for a price correction.
  • If RSI < 30 (the green region of the graph), the stock is oversold and due for a price increase.

In a broader sense:

  • A buy signal occurs when the RSI moves below 50 and then back above it.
  • A sell signal occurs when the RSI moves above 50 and then back below it.
In [15]:
fig = make_subplots(rows = 2, cols = 1, shared_xaxes = True)

fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'), row = 1, col = 1)

fig.add_trace(go.Scatter(x = stock['Date'], y = stock['RSI'], name = 'Relative Strength Index', line = dict(color = 'purple')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([50]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'black', dash = 'dot')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([30]).repeat(len(stock)), fill = 'tozeroy', showlegend = False, hoverinfo = 'skip', line = dict(color = 'green', dash = 'dash')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([70]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'red', dash = 'dash')), row = 2, col = 1)
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([100]).repeat(len(stock)), fill = 'tonexty', showlegend = False, hoverinfo = 'skip', line = dict(color = 'red')), row = 2, col = 1)

fig.update_layout(title_text = 'Relative Strength Index (RSI)')

fig.update_xaxes(title_text = 'Date', row = 2, col = 1)
fig.update_yaxes(title_text = 'Price', row = 1, col = 1)
fig.update_yaxes(title_text = 'Relative Strength Index', row = 2, col = 1)
fig.update_yaxes(nticks = 10, row = 1, col = 1)
fig.update_yaxes(nticks = 10, row = 2, col = 1)
fig.update_yaxes(fixedrange = True, row = 1, col = 1)
fig.update_yaxes(fixedrange = True, row = 2, col = 1)

fig.show()

On-Balance Volume (OBV)

The interactive line graph below shows the on-balance volume (OBV) for the stock. The OBV typically confirms trends:

  • A rising price should be accompanied by a rising OBV.
  • A falling price should be accompanied by a falling OBV.

Thus, the stock price tends to follow the direction of the on-balance volume graph.

In [16]:
fig = make_subplots(rows = 2, cols = 1, shared_xaxes = True)

fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Close'], name = 'Closing Price'), row = 1, col = 1)

fig.add_trace(go.Scatter(x = stock['Date'], y = stock['OBV'], name = 'On-Balance Volume'), row = 2, col = 1)

fig.update_layout(title_text = 'On-Balance Volume (OBV)')

fig.update_xaxes(title_text = 'Date', row = 2, col = 1)
fig.update_yaxes(title_text = 'Price', row = 1, col = 1)
fig.update_yaxes(title_text = 'On-Balance Volume', row = 2, col = 1)
fig.update_yaxes(nticks = 10, row = 1, col = 1)
fig.update_yaxes(nticks = 10, row = 2, col = 1)
fig.update_yaxes(fixedrange = True, row = 1, col = 1)
fig.update_yaxes(fixedrange = True, row = 2, col = 1)

fig.show()

Analysis

Strategy

The code below develops an investment strategy based on the stock market indicators listed above:

  • Simple Moving Average (SMA) Crossovers
  • Moving Average Convergence Divergence (MACD)
  • Relative Strength Index (RSI)
  • On-Balance Volume (OBV)

According to their values, each indicator is assigned a score ranging from -1 to 1. The total score is calculated and if it is greater than or equal to 2, the stock is given a buy rating. If the total is less than or equal to -2, the stock is given a sell rating. Otherwise, the stock is given a hold rading.

In addition, the code uses these ratings to simulate the purchase and sale of stock shares given an initial $1 million investment. The strategy is compared to a buy and hold strategy, in which the investor does not make any trades following an initial investment.

In [17]:
stock['Cash'] = pd.Series([0]).repeat(len(stock)).values
stock['Cash'][0] = 1000000
stock['Shares'] = pd.Series([0]).repeat(len(stock)).values
stock['Custom Strategy'] = pd.Series([1000000]).repeat(len(stock)).values

shares_buy_and_hold = stock['Cash'][0] // stock['Close'][0]
cash_buy_and_hold = stock['Cash'][0] - (shares_buy_and_hold * stock['Close'][0])
stock['Buy and Hold Strategy'] = pd.Series([1000000]).repeat(len(stock)).values

def strategy(i):
    
    if i > 0:
        stock['Cash'][i] = stock['Cash'][i - 1]
        stock['Shares'][i] = stock['Shares'][i - 1]
    stock['Custom Strategy'][i] = stock['Cash'][i] + stock['Shares'][i] * stock['Close'][i]
    stock['Buy and Hold Strategy'][i] = cash_buy_and_hold + shares_buy_and_hold * stock['Close'][i]
        
    info = stock.loc[i].squeeze()
    date = info['Date']
    
    indicators = {'Date' : date, 'SMA' : 0.0, 'MACD' : 0.0, 'RSI' : 0.0, 'OBV' : 0.0, 'Total' : 0.0, 'Rating' : 'Hold'}

    if info['SMA50'] > info['SMA200']:
        indicators['SMA'] += 1.0
    elif info['SMA50'] < info['SMA200']:
        indicators['SMA'] -= 1.0

    if info['MACD'] > 0:
        indicators['MACD'] += 2/3
    elif info['MACD'] < 0:
        indicators['MACD'] -= 2/3
    if info['MACD'] > info['MACDSignal']:
        indicators['MACD'] += 1/3
    elif info['MACD'] < info['MACDSignal']:
        indicators['MACD'] -= 1/3
    if info['MACDHist'] > 0 and info['MACDHist'] < info['MACDHist_SMA']:
        indicators['MACD'] -= 1/3
    elif info['MACDHist'] < 0 and info['MACDHist'] >info['MACDHist_SMA']:
        indicators['MACD'] += 1/3

    if info['RSI'] < 30:
        indicators['RSI'] += 1.0
    elif info['RSI'] > 70:
        indicators['RSI'] -= 1.0
    elif info['RSI_SMA'] < 50 and info['RSI'] > 50:
        indicators['RSI'] += 2/3
    elif info['RSI_SMA'] > 50 and info['RSI'] < 50:
        indicators['RSI'] -= 2/3
    
    if info['OBV'] > info['OBV_SMA']:
        indicators['OBV'] += 1.0
    elif info['OBV'] < info['OBV_SMA']:
        indicators['OBV'] -= 1.0
    
    indicators['Total'] = indicators['SMA'] + indicators['MACD'] + indicators['RSI'] + indicators['OBV']
    
    if indicators['Total'] >= 2.0:
        indicators['Rating'] = 'Buy'
    elif indicators['Total'] <= -2.0:
        indicators['Rating'] = 'Sell'

    if indicators['Rating'] == 'Buy':
        shares = info['Cash'] // info['Close']
        cash = shares * info['Close']
        if stock['Cash'][i] - cash >= 0:
            stock['Cash'][i] -= cash
            stock['Shares'][i] += shares
            
    elif indicators['Rating'] == 'Sell':
        shares = info['Shares']
        cash = shares * info['Close']
        if stock['Shares'][i] - shares >= 0:
            stock['Cash'][i] += cash
            stock['Shares'][i] -= shares
            
    return indicators

Recommendation

The code below uses the strategy documented above to indicate whether an investor should buy, sell, or hold the stock.

In [18]:
print(strategy(len(stock) - 1)['Rating'])
Hold

Simulation

The code below implements the simulation described above using stock market data from the past 5 years to see the results of the strategy given a $1 million investment.

In [19]:
for i in range(len(stock)):
    strategy(i)
stock
Out[19]:
Date High Low Open Close Volume Adj Close SMA200 SMA50 EMA26 ... MACDHist MACDHist_SMA RSI RSI_SMA OBV OBV_SMA Cash Shares Custom Strategy Buy and Hold Strategy
0 2015-02-10 184.80 182.35 183.35 184.56 2556300 171.20 NaN NaN NaN ... NaN NaN NaN NaN 2556300 NaN 1000000 0 1000000 1000000
1 2015-02-11 188.19 183.75 184.07 187.65 4079600 174.07 NaN NaN NaN ... NaN NaN NaN NaN 6635900 NaN 1000000 0 1000000 1016741
2 2015-02-12 190.00 187.40 188.25 189.78 3086700 176.04 NaN NaN NaN ... NaN NaN NaN NaN 9722600 NaN 1000000 0 1000000 1028281
3 2015-02-13 191.33 188.34 189.87 189.00 2720400 175.32 NaN NaN NaN ... NaN NaN NaN NaN 7002200 NaN 1000000 0 1000000 1024055
4 2015-02-17 190.63 188.31 188.78 190.02 2123300 176.27 NaN NaN NaN ... NaN NaN NaN NaN 9125500 NaN 1000000 0 1000000 1029582
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1253 2020-02-03 242.39 238.05 238.36 239.01 2733000 239.01 212.15 231.87 238.99 ... -1.40 -0.55 50.37 57.85 22441200 23373980.0 133 5963 1425349 1295010
1254 2020-02-04 243.74 241.55 242.88 241.94 3052500 241.94 212.33 232.35 239.21 ... -1.22 -0.75 54.27 56.47 25493700 23255610.0 133 5963 1442821 1310884
1255 2020-02-05 245.13 243.00 244.99 244.30 3126600 244.30 212.53 232.87 239.59 ... -0.93 -0.89 57.19 55.23 28620300 23246810.0 133 5963 1456893 1323671
1256 2020-02-06 245.77 241.18 245.35 241.82 2225600 241.82 212.72 233.30 239.75 ... -0.88 -1.00 53.34 53.98 26394700 23296130.0 133 5963 1442105 1310234
1257 2020-02-07 240.52 236.55 239.75 238.00 3006500 238.00 212.91 233.61 239.62 ... -1.08 -1.08 47.98 53.03 23388200 23332540.0 133 5963 1419327 1289537

1258 rows × 24 columns

Portfolio Value

The code below creates an interactive graph comparing your portfolio value when using:

  • The custom strategy (which takes advantage of technical indicators)
  • The buy and hold strategy (which is used as a standard of comparison)
In [20]:
fig = go.Figure()
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Custom Strategy'], name = 'Custom Strategy'))
fig.add_trace(go.Scatter(x = stock['Date'], y = stock['Buy and Hold Strategy'], name = 'Buy and Hold Strategy'))
fig.add_trace(go.Scatter(x = stock['Date'], y = pd.Series([1000000]).repeat(len(stock)), showlegend = False, hoverinfo = 'skip', line = dict(color = 'black', dash = 'dot')))
fig.update_layout(title_text = 'Portfolio Value', 
                  xaxis_title = 'Date',
                  yaxis_title = 'Portfolio Value',
                  xaxis_rangeslider_visible = True,
                  showlegend = True)
fig.update_yaxes(nticks = 10)
fig.show()